Skip to content

feat(transform): object-level schema routing via SchemaRouter - #319

Merged
pyramation merged 1 commit into
mainfrom
feat/schema-transform-object-routing
Jul 29, 2026
Merged

feat(transform): object-level schema routing via SchemaRouter#319
pyramation merged 1 commit into
mainfrom
feat/schema-transform-object-routing

Conversation

@pyramation

Copy link
Copy Markdown
Collaborator

Summary

Generalizes the schema transform from a whole-schema Map<oldSchema, newSchema> to a SchemaRouter that resolves a target schema per object occurrence(schema, name, namespace) — instead of per schema. This makes a single source schema fan out at the object level: e.g. route a table to one schema and a function to another, with every cross-object reference (SQL bodies, PL/pgSQL bodies, DROP/revert statements, types, params, return types) rewritten to each object's routed target.

Motivation: downstream (pgpm apply modules) needs to cherry-pick objects out of a reusable package into consumer-selected schemas — e.g. a shared helper function in reporting that reads a per-tenant tenant_a.accounts table, from a single users-schema source. A Map<string,string> can only send all of users to one place.

Model

SchemaRouter mirrors the routing shape already used by qualifyUnqualified (relations / functions / types buckets → matching pg_class / pg_proc / pg_type):

new SchemaRouter({
  users: {
    schema: 'tenant_a',                       // schema-level default (optional)
    functions: { account_count: 'reporting' } // per-object override
  }
});

Resolution is object-route-first, then the schema-level default, then leave unchanged. Namespaces are independent (a table widget and a function widget can route to different schemas). SchemaRouter.fromSchemaMap(...) reproduces the historic whole-schema behaviour exactly.

Changes

  • router.ts (new)SchemaRouter, SchemaRoute, RouteSpec, ObjectNamespace, RouteNamespace.
  • Threaded an object namespace through transformNameList / transformRelation; annotated create & reference sites (functions, types, relations) and DROP … (namespace derived from removeType, so revert scripts route correctly).
  • LANGUAGE sql bodies (opaque String nodes) are rewritten AST-precisely (parse → walk → deparse) when object routes are present; pure whole-schema routes keep the existing quoting-preserving string pass — so existing behaviour is byte-for-byte unchanged.
  • validateNoUntransformedSchemas now enforces the leftover check only for fully-moved schemas (those with a schema-level default); partially/object-only routed schemas may legitimately retain references.
  • transformSql / transformSqlStatement accept Map<string,string> | SchemaRouter (SchemaMappingInput). assumeSchemasExist and SchemaTransformResult reporting are preserved.

Backwards compatibility

Map<string,string> remains fully supported — it is coerced to a schema-level-only router. All 160 existing tests pass unchanged.

Tests

12 new tests (__tests__/router.test.ts): router unit behaviour (override/fallback/namespace independence/fully-moved reporting), table+function fan-out from one schema, cross-object refs in SQL and PL/pgSQL bodies, DROP routing by namespace, assumeSchemasExist with a router (CREATE SCHEMA IF NOT EXISTS), and Map/router output parity. Round-trip validation enabled on the full-module test.

Test Suites: 5 passed, 5 total
Tests:       172 passed, 172 total

Notes

  • pnpm run build succeeds. pnpm run lint fails repo-wide with a pre-existing ESLint 9 vs legacy .eslintrc.json mismatch (couldn't find an eslint.config.(js|mjs|cjs)), unrelated to this change.
  • Consumed downstream by constructive-io/constructive (pgpm object-level apply routing) — PR linked separately.

Link to Devin session: https://app.devin.ai/sessions/025fb88043964fdbb335ac5e39df2478
Requested by: @pyramation

Generalize the schema transform from a whole-schema Map<oldSchema,newSchema>
to a SchemaRouter that resolves a target schema per (schema, name, namespace).
This lets a single source schema fan out per object — e.g. route a table to
one schema and a function to another — while cross-object references (SQL and
PL/pgSQL bodies, DROP/revert statements) are rewritten to each object's routed
target.

- Add SchemaRouter (relations/functions/types buckets + schema-level default),
  mirroring the routing model in qualifyUnqualified.
- Thread an object namespace through transformNameList/transformRelation and
  annotate create/reference sites (functions, types, relations, drops).
- Rewrite LANGUAGE sql function bodies AST-precisely when object routes exist;
  whole-schema routes keep the existing string pass (fully backwards compatible).
- validateNoUntransformedSchemas only enforces fully-moved (schema-default)
  schemas; partially-routed schemas may legitimately retain references.
- transformSql/transformSqlStatement accept a Map or a SchemaRouter.

160 existing tests unchanged; 12 new tests cover table/function fan-out,
SQL + PL/pgSQL cross-object body refs, DROP routing, and Map/router parity.
@pyramation pyramation self-assigned this Jul 29, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit d7d96e0 into main Jul 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant